package cgl.narada.service.security;

import java.math.BigInteger;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Hashtable;
import java.util.Vector;

import org.apache.log4j.Logger;

import cgl.narada.event.EventHeaders;
import cgl.narada.event.NBEvent;
import cgl.narada.event.NBEventException;
import cgl.narada.service.ServiceException;
import cgl.narada.service.security.impl.EntityOperationsImpl;
import cgl.narada.service.security.kmc.SignedSecurityToken;
import cgl.narada.service.security.kmc.TimeStampedSeqNo;
import cgl.narada.service.security.securityprovider.CertificateManager;
import cgl.narada.service.security.securityprovider.CertificateUtil;
import cgl.narada.service.timer.ClockI;
import cgl.narada.service.timer.HRClock;
import cgl.narada.util.SystemInit;

/**
 * The purpose of this class is to keep track of currently available secure
 * topics. The manager listens on a KMC broadcast topic. The KMC broadcasts a
 * message whenever a new security token is created. The KMC may also publish
 * information regarding revoked certificates, invalidated security tokens etc..
 * <br>
 * The manager also provides interface to validate security tokens and digital
 * signatures of payloads in secure NB events <br>
 * 
 * Created on Sep 10, 2005
 * 
 * @author Harshawardhan Gadgil (hgadgil@grids.ucs.indiana.edu)
 */
public class SecureTopicsManager {

    static Logger log = Logger.getLogger("SecureTopicsManager");
    static {
        SystemInit.init();
    }

    public static final String HEADER_SIGNATURE = "_headerSig";

    /** Maintain a list of revoked certificate Serial numbers */
    private Vector revokedCertificates;
    private PublicKey rootCAPublicKey;

    private Vector secureTopicsList;

    private Vector validKMCList;

    private Hashtable profileTokenMappingTable;

    // BENCHMARK
    boolean benchmark = false;
    ClockI hrClock = null;
    private static long signedTSSecurityCheckDuration;
    private long start = 0, end = 0;
    private String moduleName = "SecureTopicsManager: ";

    /**
     * Table that maintains a list of timestamps for entity ids who publish
     * secure events
     */
    private Hashtable entityIDTimeStampMapping;

    private EntityOperationsImpl entityOperations = null;


    public SecureTopicsManager() {
        revokedCertificates = new Vector();
        secureTopicsList = new Vector();
        validKMCList = new Vector();
        profileTokenMappingTable = new Hashtable();
        entityIDTimeStampMapping = new Hashtable();
        loadCertificateListFromPersistentStorage();

        if (benchmark) {
            hrClock = HRClock.getClock();
        }
        try {
            entityOperations = new EntityOperationsImpl();
        } catch (ServiceException e) {
          System.out.println(moduleName + "Problems initializing " +
                             "EntityOperations");    
        }
    }

    /**
     * Initializes the Certificate manager to load the location of keystore
     * etc...
     * 
     * @param configFileName
     */
    public void init(String keyStore) {

        CertificateManager certMan = new CertificateManager();
        certMan.init(keyStore, null);
        rootCAPublicKey = CertificateUtil.getPublicKey(certMan,
                certMan.ROOT_CA_ALIAS);
    }

    public void shutdown() {
        revokedCertificates.clear();
        secureTopicsList.clear();
        profileTokenMappingTable.clear();
        entityIDTimeStampMapping.clear();
    }

    public boolean doSecurityChecks(NBEvent nbEvent) {
        EventHeaders hdr = nbEvent.getEventHeaders();

        log.info("SECURITY CHECK:");

        boolean eventOnSecureTopic = false;

        // If event's content sysnopsis is previously registered asa secure
        // topic name
        if (sysnopsisRepresentsSecureTopic(nbEvent.getContentSynopsis(),
                nbEvent.getContentSynopsisType())) {
            eventOnSecureTopic = true;
            log.info("\t-> Event published on secure Topic !");
        }

        // Is it a secure event ?
        if (hdr.isSecure()) {
            log.info("\t-> Secure event published !");

            if (!verifyNBEvent(nbEvent)) {
                // Hmm, we have a problem... the secure topics manager could
                // not verify the event payload...so NUKE this event
                return false;
            }

            log.info("\t-> Event Verified !");

            // Security timestamp check
            // If its a secure event, then for this sended update the timestamp
            // associated with the event...

            // BENCHMARK (Check for replay by comparing timestamps)
            if (benchmark) {
                start = hrClock.getTimeMicroseconds();
            }

            // Check the time stamp for this sender
            Integer entityId = new Integer(nbEvent.getEventHeaders()
                    .getSource());
            TimeStampedSeqNo stssn;

            byte[] stssnBytes = nbEvent.getEventHeaders()
                    .getSecurityTimeStampedSeqNo();

            stssn = TimeStampedSeqNo.unmarshal(stssnBytes);

            if (stssn == null) {
                // Something is wrong with this event...Discard it...
                log.error("Secure Event does not contain "
                        + "Time Stamped Sequence No. !");
                return false;
            }

            // System.out.println("TimeStamp: " + stssn.toString());

            TimeStampedSeqNo lastTimeStamp = (TimeStampedSeqNo) entityIDTimeStampMapping
                    .get(entityId);
            if (lastTimeStamp == null) {
                // This is the first event from this guy.. so simply store
                // his timestamp...
                System.out.println("New Timestamp Entry !");

                entityIDTimeStampMapping.put(entityId, stssn);
            } else {
                // Check if the timestamp in the event is monotonically
                // increasing...
                if (stssn.isGreater(lastTimeStamp)) {
                    // Good..replace lasttimestamp with the new timestamp

                    entityIDTimeStampMapping.put(entityId, stssn);
                } else {
                    // Aaha.. Duplicate time stamp... discard event...
                    log.error("STSSN<" + nbEvent.getContentSynopsis() + "> is "
                            + "<= last known timestamp");
                    return false;
                }
            }

            // BENCHMARK (ONLY FOR secure events)
            if (benchmark) {
                end = hrClock.getTimeMicroseconds();
                signedTSSecurityCheckDuration = end - start;
                // System.out.println("TSSCheck Duration(microSec): ["+end+" -
                // "+start+"] = " + signedTSSecurityCheckDuration);
            }

            // Ok secure topic + valid event
            // if this topic is still not registered as a secure topic then
            // register the topic synopsis in the secure topics list
            if (!eventOnSecureTopic) {
                secureTopicsList.add(nbEvent.getContentSynopsis());
                log.info("Registering Secure Topic: "
                        + nbEvent.getContentSynopsis());
            }

            return true;
        } else {
            // Event recieved on secure topic but no security headers in event
            if (eventOnSecureTopic) {
                log.debug("\t-> Non-secure event published on secure topic !");
                return false;
            } else {
                log.debug("\t-> NON-Secure Topic + Non Secure event == Cool !");
                return true;
            }
        }
    }

    private boolean verifyNBEvent(NBEvent event) {

        EventHeaders headers = event.getEventHeaders();

        log.info("Verifying NBEvent !!");

        if (!headers.hasSignature()) {
            log.error("No security token found !");
            return false;
        }

        // Verify the Signed security token's certificate
        SignedSecurityToken sst = SignedSecurityToken
                .createObjectFromBytes(headers.getSignedSecurityToken());

        // Step 1: Verify the certificate with Root CA's public key
        Certificate cert = sst.getKmcCertificate();
        if (!CertificateUtil.ValidateCertificate(cert, rootCAPublicKey)) {
            log.error("KMC's certificate could not be validated !");
            return false;
        }

        // Step 2: Now check the payload's signature
        // Step 2a: Get the entity's certificate from the certificate's table
        Certificate entityCert = sst.getSecurityToken()
                .getTopicOwnerCertificate();

        // TODO: Is there a better way to deal with revoked Certificates ?
        BigInteger certId = ((X509Certificate) entityCert).getSerialNumber();

        // Check if the certificate is in the revoked list
        if (revokedCertificates.contains(certId)) {
            log.error("Entity's certificate has been revoked !");
            return false;
        }

        // Verify the certificate
        if (!CertificateUtil.ValidateCertificate(entityCert, rootCAPublicKey)) {
            log.error("Entity's certificate could not be validated !");
            return false;
        }

        // Validate the Signed Security token
        if (!sst.verify()) {
            log.error("Could not verify integrity of signed security token !");
            return false;
        }

        // Check the signature
        try {
            // Check Header signature
            byte[] sig;
            try {
                sig = (byte[]) event.getEventProperties().getProperty(
                        HEADER_SIGNATURE);
            } catch (NBEventException e) {
                sig = null;
            }

            if (sig == null) {
                log.error("Header signature not found !");
                return false;
            }

            String signingAlgorithm = headers.getSignatureAlgorithm();

            if (!entityOperations.validateSignature(entityCert.getPublicKey(),
                    signingAlgorithm, "BC", headers.getBytes(), sig)) {
                log.error("Could not validate Event header signature !");
                return false;
            }

            // Check payload signature
            if (!entityOperations.validateSignature(entityCert.getPublicKey(),
                    signingAlgorithm, "BC", event.getContentPayload(), headers
                            .getSignature())) {
                log.error("Could not validate Event payload signature !");
                return false;
            }
        } catch (ServiceException e) {
            log.error("", e);
            return false;
        }

        // Finally check the signature of the headers and see if they were
        // modifies

        // Ok, Everything checks FINE... this event is good to go !
        return true;
    }

    /**
     * Checks to see if the specified content synopsis belongs to a secure
     * topic.
     * 
     * @param contentSynopsis
     * @param type
     * @return TRUE if the content synopsis refers to a secure topic, FALSE
     *         otherwise
     */
    public boolean sysnopsisRepresentsSecureTopic(Object contentSynopsis,
            int type) {

        // TODO: Investigate if we need a better matching than this
        boolean x = secureTopicsList.contains(contentSynopsis);
        log.info("Is <" + contentSynopsis + "> a Secure Topic :: " + x);
        return x;
    }

    public void registerProfileSecurityToken(Object key, byte[] obj) {
        SignedSecurityToken sst = SignedSecurityToken
                .createObjectFromBytes(obj);
        log.info("******* \n Registering Security Token for Profile: " + key);
        profileTokenMappingTable.put(key, sst);
    }

    /**
     * Returns the Security token corresponding to the given id
     * 
     * @param key -
     *            Id
     * @return Security Token if valid, NULL otherwise
     */
    public SignedSecurityToken getSecurityTokenForProfileId(Object key) {
        SignedSecurityToken sst = (SignedSecurityToken) profileTokenMappingTable
                .get(key);
        if (sst != null) {
            log.debug("Non-null Security Token !");
            if (sst.getSecurityToken().isValid()) {
                log.debug("Valid Token !!");
                return sst;
            } else {
                // Hmmm found an invalid security token, so delete it ...
                log.warn("Expired Token ! Deleting !!");
                profileTokenMappingTable.remove(key);
            }
        }
        log.warn("No matching Token for key: " + key);
        return null;
    }

    /**
     * Check the validity and integrity of a signed security token. Finally
     * determines if the requesting entity does indeed have subscribe rights
     * 
     * @param sst -
     *            The SignedSecuritytoken to inspect
     * @return TRUE if everything check out, FALSE otherwise
     */
    public boolean verifySignedSecurityToken(SignedSecurityToken sst) {
        System.out.print("STM: Verify SST...");

        // First check if the sst is valid (check date)
        if (!sst.getSecurityToken().isValid()) {
            log.error("Signed Security Token has expired !");
            return false;
        }

        // check the signature
        if (!sst.verify()) {
            log
                    .error("Could not verify signature in the SignedSecurityToken !");
            return false;
        }

        // Check if the kmc is a valid kmc
        X509Certificate kmcCert = (X509Certificate) sst.getKmcCertificate();
        if (!validKMCList.contains(kmcCert.getSubjectDN().getName())) {
            log.error("The security token signer is not a valid KMC !");
            return false;
        }

        // Check if the KMC's certificate is valid
        if (!CertificateUtil.ValidateCertificate(kmcCert, rootCAPublicKey)) {
            log.error("KMC's certificate could not be validated !");
            return false;
        }

        log.debug("VERIFIED ! Subscription OKed");
        return true;
    }

    private void loadCertificateListFromPersistentStorage() {
        validKMCList.add("CN=KMC, OU=CGL, O=IU, L=Bloomington, C=US");
        // TODO: Implement this !!
    }

    // BENCHMARK TIME GETTER
    /**
     * @return Returns the signedTSSecurityCheckDuration.
     */
    public static long getSignedTSSecurityCheckDuration() {
        return signedTSSecurityCheckDuration;
    }

}
